Repository navigation
fix(capture): preserve token capture after synthetic completions - #3670
Merged
Merged
Conversation
Track normal worker completions per request so token-limit and reasoning-guard responses remain uncommitted without poisoning earlier captured turns. Preserve acknowledgement validation for actual worker responses across vLLM and Megatron. Add 44 regression cases across handlers, API routes, and JSON/SSE. Validation: 1,055 focused tests and real vLLM rollouts pass in Slurm; all-files pre-commit passes. Signed-off-by: Pranav Thombre <pthombre@nvidia.com>
Contributor
Author
|
/ok to test 3c8f7ba |
pthombre
marked this pull request as ready for review
September 24, 2026 00:40
ananthsub
previously approved these changes
Sep 28, 2026
Address review on #3670. Document in the ExternalCaptureHandler protocol that prepare_response() must run for every worker completion, since finalize_response() now commits or poisons a call only after that mark is set. Reword the early-return comment to distinguish synthetic completions (no worker completion at all) from a worker completion whose acknowledgement is missing. Add a route-level regression test that drops ng_commit_coords from a real worker completion and asserts the call still fails closed with worker_response_missing_commit_coordinates, for both backends with and without streaming. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Pranav Thombre <pthombre@nvidia.com>
Contributor
Author
|
/ok to test 22761ca |
1 similar comment
Contributor
Author
|
/ok to test 22761ca |
ananthsub
approved these changes
Sep 29, 2026
ananthsub
enabled auto-merge
September 29, 2026 23:09
Contributor
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/36644628896 |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes external token capture when Gym serves an empty synthetic completion after a vLLM context/token-limit error or the sequential-reasoning guard.
The served-response finalization introduced in #3301 correctly commits fingerprints after API conversion, but it also runs for responses that did not come from a normal worker completion. Two existing paths hit this case:
max_tokensis converted into an empty completion withfinish_reason="length".sequential_reasoning_allowed=false, a continuation that hits the reasoning guard returns an empty completion withfinish_reason="content_filter".Neither path reaches the worker-response preparation hook, so neither has worker commit coordinates. Finalization currently records
worker_response_missing_commit_coordinates, which can poison the entire rollout and discard valid tokens captured in earlier turns. For example, a successful tool-use turn followed by a context-limit completion loses its already-staged training row.This PR adds a request-scoped
external_worker_response_seenflag and sets it only when a normal worker completion reachesprepare_response(). If no worker completion was received, the shared finalizer returns without committing or poisoning the call. Existing middleware still recordsrequest_finished_without_staged_coordinates; a consumer selecting the preceding committed terminal can retain the valid token chain.The fix lives in the shared handler introduced by #2823 and covers both vLLM and Megatron worker capture. A real worker completion missing its acknowledgement still fails with
worker_response_missing_commit_coordinates; malformed acknowledgements and explicit worker capture failures retain their existing behavior. Sending a request alone does not set the flag, because the backend may return a context-limit error.Adds 44 regression cases covering both backends, JSON and buffered SSE, Chat Completions/Responses/Messages/compaction routes, overflow before or after a committed call, and reasoning-only continuations. The tests verify lineage, preserved tokens/masks/logprobs, request isolation, and transport-field stripping. A synthetic response explicitly selected as the terminal remains unattributable; an initial overflow creates no trainable row.
No separate issue is needed for this focused correction to the existing capture lifecycle; the reproduction and affected behavior are documented here.
Validation
pre-commit run --all-filespassed before publishing, andgit diff --checkpassed.Container used for both Slurm jobs:
Validation commands and local artifacts
Job submissions from the fix worktree:
The native validation runner invokes pytest through a bootstrap that selects the current checkout and asserts the imported Gym/model-server code comes from that checkout. Its passing test command uses this selection and configuration:
python cache/worker-bypass-validation/pytest_bootstrap.py \ -o addopts= -o log_cli=false --import-mode=importlib -q --tb=short \ tests/unit_tests/test_external_capture_handlers.py \ tests/unit_tests/test_base_responses_api_model.py \ tests/unit_tests/test_chat_completions_streaming.py \ tests/unit_tests/test_responses_api_model_streaming.py \ tests/unit_tests/test_token_id_capture.py \ tests/unit_tests/test_token_capture*.py \ responses_api_models/vllm_model/tests \ responses_api_models/vllm_model_with_compaction/tests \ --cov=nemo_gym.token_id_capture.sink \ --cov=nemo_gym.token_id_capture.external_capture \ --cov-report=term-missing --cov-fail-under=0 \ --cov-config=cache/worker-bypass-validation/coverage.ini \ --cov-report=xml:cache/worker-bypass-validation/coverage.xml \ --junitxml=cache/worker-bypass-validation/regressions.xmlThe temporary baseline checkout runs the two changed test files with
-k synthetic_completion. The GPU smoke job runspython cache/worker-bypass-validation/real_worker_smoke.py.All-files hooks were invoked with:
Validation scripts, logs, JUnit XML, coverage, and rollout evidence are local artifacts, not committed source. They are retained under:
Key artifacts:
results.json,baseline.xml,regressions.xml,pre-commit-all-files.log, andreal-rollout/evidence.json.Rollouts
Slurm GPU job 3966328 completed successfully using Qwen/Qwen3-0.6B, vLLM 0.25.1, and the real NeMo RL HTTP worker with local durable staging, served through Gym Responses SSE:
add(2, 2)tool rollout returned4, passed the smoke verifier (reward 1.0), and produced two chained committed calls: 234 total tokens, including 27 training tokens, with finite logprobs.This is a vLLM real-model smoke test; Megatron behavior is covered by the parameterized handler and route tests, not a live Megatron deployment.
Compatibility and benchmark impact
No public API, configuration, ledger/staging schema, or migration changes. Synthetic API responses keep their existing finish reasons. Valid earlier generations can remain trainable when the rollout ends in a synthetic completion, eliminating this source of dropped training rows. Missing acknowledgements from actual worker completions still fail closed. Benchmark scoring logic is unchanged.
Documentation: N/A; this restores existing capture behavior without adding user configuration or a public interface.
Checklist
pre-commit run --all-filespasses.